"""
======
m.core
======

The module that contains all sorts of really basic (but mostly also really important) things, including the lexical analyzer.

Some of the analysis techniques are partially borrowed from the Django framework.

"""

import re

from m.config import *
from m.exceptions import ConditionError, InvalidArgument, InvalidOperator, NoSuchSubtag

class ArgumentParser:
    """
    ArgumentParser
    ==============

    A class for parsing arguments, be they plain strings, integers, floats or context variables..
    
    """
    def __init__(self, context):
        """
        ArgumentParser.__init__
        =======================

        __init__(self, context) -> ArgumentParser instance
        Instantiate the parser, storing the sent ``context``, which will be used later on.
        
        """
        self.context = context

    def parse(self, argument):
        """
        ArgumentParser.parse
        ====================

        parse(self, argument)
        Parse the sent ``argument`` in the context stored from instantiation.
        
        """
        return self.parse_one(self.context, argument)

    @classmethod
    def parse_one(cls, context, argument):
        """
        ArgumentParser.parse_one
        ========================

        @classmethod
        parse_one(cls, context, argument)
        Parse a single argument; this is essentially what ``ArgumentParser.parse`` does, only in a more convenient way.
        
        """
        if argument.startswith('"'):
            assert argument.endswith('"'), SyntaxError('Unterminated string')
            return argument[1:-1]
        else:
            try:
                value = int(argument)
            except ValueError:
                try:
                    value = float(argument)
                except ValueError:
                    bits = argument.split('.')
                    value = context
                    for bit in bits:
                        try:
                            value = value[bit]
                        except (KeyError, TypeError, AttributeError), e:
                            try:
                                value = getattr(value, bit)
                            except AttributeError:
                                try:
                                    value = value[int(bit)]
                                except ValueError:
                                    raise InvalidArgument("Argument not found in context: %s" % argument)

            return value

    @staticmethod
    def parse_arguments(plain):
        """
        ArgumentParser.parse_arguments
        ==============================

        todo: decide if this really belongs here.

        @staticmethod
        parse_arguments(plain) -> list of arguments
        Parse a space-string into several arguments, where double quotes bind a string containing spaces together.
        
        """
        bits = plain.split()
        out = []
        in_q = False
        for bit in bits:
            if in_q:
                if bit.endswith('"'):
                    out.append(temp + bit)
                    in_q = False
                else:
                    temp += bit + ' '
            else:
                if bit.startswith('"'):
                    if bit.endswith('"'):
                        out.append(bit)
                    else:
                        in_q = True
                        temp = bit + ' '
                else:
                    out.append(bit)

        return out

class Node:
    """
    Node
    ====

    The base node class, which all other (builtin and custom) nodes must subclass.
    
    """
    block = False

class Tag(Node):
    """
    Tag
    ===

    A ``Node`` subclass which does just a bit more.
    
    """
    block = True
    keyword = '?'
    def __init__(self, value, body):
        """
        Tag.__init__
        =============

        __init__(self, value, body) -> Tag instance
        Instantiate the tag.
        
        """
        self.subtags = []
        self.value, self.body = ArgumentParser.parse_arguments(value), body

    def add_sub(self, tag, body):
        """
        Tag.add_sub
        ============

        add_sub(self, tag, body)
        Add a subtag to the tag.
        
        """
        self.subtags.append((tag, body))

    def get_sub(self, tag):
        """
        Tag.get_sub
        ===========

        get_sub(self, tag) -> NodeList instance
        Retrieve a stored subtag by its identifier.
        
        """
        for subtag, body in self.subtags:
            if subtag == tag or subtag.split()[0] == tag:
                return body

        return False

    def get_subs(self, tag):
        """
        Tag.get_subs
        ============

        get_subs(self, tag)
        A generator which returns all applicable subs of a certain 'type'.
        
        """
        for subtag, body in self.subtags:
            if subtag == tag or subtag.split()[0] == tag:
                yield (subtag, body)

class NodeList(list):
    """
    NodeList
    ========

    A list subclass designed specifically for templating.
    
    """
    def __repr__(self):
        """
        NodeList.__repr__
        =================

        __repr__(self) -> string
        A textual representation of the class.
        
        """
        return 'NodeList[%s]' % ', '.join(repr(item) for item in self)

    def render(self, context):
        """
        NodeList.render
        ===============

        render(self, context) -> string
        Render all contained nodes using the sent ``context`` dictionary.
        
        """
        parts = []
        for node in self:
            parts.append(node.render(context))

        return ''.join(parts)

class Token:
    """
    Token
    =====

    A basic token class, which is a more convenient way of holding token information.
    
    """
    def __init__(self, id, contents, node=Node):
        """
        Token.__init__
        ==============

        __init__(self, id, contents, node=None) -> Token instance
        Instantiate the token class.
        
        """
        self.id, self.contents, self.Node = id, contents, node

    def __repr__(self):
        """
        Token.__repr__
        ==============

        __repr__(self) -> string
        A textual representation of the token.
        
        """
        return '<Token (%d): %s>' % (self.id, self.contents)

class Lexer:
    """
    Lexer
    =====

    The lexical analyzer, which consists of two parts:
    - The tokenizer (or scanner), which scans the string sent and converts it into a series of tokens
    - The evaluator, which takes the tokens from the tokenizer and attempts to describe them as different token kinds (currently *text*, *variables* and *tags*).
    
    """
    def __init__(self, data):
        """
        Lexer.__init__
        ==============

        __init__(self, data) -> Lexer instance
        Instantiate the lexical analyzer and store the data sent. No tokenization is done at this point.
        
        """
        from m.common import TextNode, PrintNode
        self.Text, self.Print = TextNode, PrintNode
        
        self.data = data

    def analyze(self, tags):
        """
        Lexer.analyze
        =============

        analyze(self, tags) -> list of ``Token``s
        Do the actual analysis of the data sent on instantiation. The ``tags`` argument takes a dictionary of tags identifiers and corresponding tag classes.
        
        """
        self.tags = tags
        
        tokens = token_regexp.split(self.data)
        return [self.describe(token) for token in tokens]

    def describe(self, token):
        """
        Lexer.describe
        ==============

        todo: fix some inconsistency within this method.

        describe(self, token) -> Token instance
        Describe a token and 'compile' it into a ``Token`` instance.
        
        """
        if token.startswith(TAG_START) and token.endswith(TAG_END):
            content = token[len(TAG_START):-len(TAG_END)].strip()
            data = content.split(' ', 1)
            name, args = data[0], (len(data) > 1 and data[1])

            if name in self.tags:
                return Token(TAG_ID, args, self.tags[name])
            else:
                return Token(TAG_ID, (args and '%s %s' % (name, args) or name), Tag)
        elif token.startswith(VARIABLE_START) and token.endswith(VARIABLE_END):
            # as of version 0.1dev2, variables are merely aliases of ``print`` nodes.
            return Token(VARIABLE_ID, token[len(VARIABLE_START):-len(VARIABLE_END)].strip(), self.Print)
        else:
            return Token(TEXT_ID, token, self.Text)

class Conditional:
    """
    Conditional
    ===========

    A conditional statement, which parses and evaluates the condition to then return one of two values.
    
    """
    expr = re.compile('(%s|\s+?[^\w]{2}\s+?)' % '|'.join('\s+?%s\s+?' % re.escape(kw) for kw in operators.keys() + ['not']))
    def __init__(self, context):
        """
        Conditional.__init__
        ====================

        __init__(self, context) -> Conditional instance
        Instantiate the conditional and store the sent ``context``.
        
        """
        self.context = context

    def evaluate(self, condition):
        """
        Conditional.evaluate
        ====================

        evaluate(self, condition) -> boolean
        Evaluate the sent ``condition`` in the previously sent ``context``.
        
        """
        condition = [bit.strip() for bit in self.expr.split(condition) if bit]
        if condition[0] == 'not':
            negate = True
            del condition[0]
        else:
            negate = False

        assert len(condition), ConditionError("No condition supplied")
        parser = ArgumentParser(self.context)
        if len(condition) == 1:
            try:
                status = bool(parser.parse(condition[0]))
            except InvalidArgument:
                status = False

            if negate:
                status = not value
        elif len(condition) > 2:
            if len(condition) > 3:
                condition = [condition[0], condition[1], join(condition[2:])]
            left, operator, right = condition
            left, right = parser.parse(left), parser.parse(right)
            assert operator in operators, InvalidOperator("Invalid operator: '%s'" % operator)
            
            status = operators[operator](left, right)
            if negate:
                status = not status
        else:
            raise ConditionError("Invalid condition supplied")

        return status
